package com.starcor.xul.Utils;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Created by hy on 2015/6/27.
 */
public class XulBufferedInputStream extends FilterInputStream {
	/**
	 * The buffer containing the current bytes read from the target InputStream.
	 */
	protected volatile byte[] buf;

	/**
	 * The total number of bytes inside the byte array {@code buf}.
	 */
	protected int count;

	/**
	 * The current limit, which when passed, invalidates the current mark.
	 */
	protected int marklimit;

	/**
	 * The currently marked position. -1 indicates no mark has been set or the
	 * mark has been invalidated.
	 */
	protected int markpos = -1;

	/**
	 * The current position within the byte array {@code buf}.
	 */
	protected int pos;

	/**
	 * Constructs a new {@code BufferedInputStream}, providing {@code in} with a buffer
	 * of 8192 bytes.
	 *
	 * <p><strong>Warning:</strong> passing a null source creates a closed
	 * {@code BufferedInputStream}. All read operations on such a stream will
	 * fail with an IOException.
	 *
	 * @param in the {@code InputStream} the buffer reads from.
	 */
	public XulBufferedInputStream(InputStream in) {
		this(in, 8192);
	}

	/**
	 * Constructs a new {@code BufferedInputStream}, providing {@code in} with {@code size} bytes
	 * of buffer.
	 *
	 * <p><strong>Warning:</strong> passing a null source creates a closed
	 * {@code BufferedInputStream}. All read operations on such a stream will
	 * fail with an IOException.
	 *
	 * @param in the {@code InputStream} the buffer reads from.
	 * @param size the size of buffer in bytes.
	 * @throws IllegalArgumentException if {@code size <= 0}.
	 */
	public XulBufferedInputStream(InputStream in, int size) {
		super(in);
		if (size <= 0) {
			throw new IllegalArgumentException("size <= 0");
		}
		buf = new byte[size];
	}

	public void resetInputStream(InputStream is) throws IOException {
		InputStream localIn = in;
		this.in = is;
		this.count = 0;
		this.marklimit = 0;
		this.markpos = -1;
		this.pos = 0;
		if (localIn != null) {
			localIn.close();
		}
	}

	/**
	 * Returns an estimated number of bytes that can be read or skipped without blocking for more
	 * input. This method returns the number of bytes available in the buffer
	 * plus those available in the source stream, but see {@link InputStream#available} for
	 * important caveats.
	 *
	 * @return the estimated number of bytes available
	 * @throws IOException if this stream is closed or an error occurs
	 */
	@Override
	public synchronized int available() throws IOException {
		InputStream localIn = in; // 'in' could be invalidated by close()
		if (buf == null || localIn == null) {
			throw streamClosed();
		}
		return count - pos + localIn.available();
	}

	private IOException streamClosed() throws IOException {
		throw new IOException("BufferedInputStream is closed");
	}

	/**
	 * Closes this stream. The source stream is closed and any resources
	 * associated with it are released.
	 *
	 * @throws IOException
	 *             if an error occurs while closing this stream.
	 */
	@Override
	public void close() throws IOException {
		resetInputStream(null);
	}

	private int fillbuf(InputStream localIn, byte[] localBuf)
		throws IOException {
		if (markpos == -1 || (pos - markpos >= marklimit)) {
            /* Mark position not set or exceeded readlimit */
			int result = localIn.read(localBuf);
			if (result > 0) {
				markpos = -1;
				pos = 0;
				count = result == -1 ? 0 : result;
			}
			return result;
		}
		if (markpos == 0 && marklimit > localBuf.length) {
            /* Increase buffer size to accommodate the readlimit */
			int newLength = localBuf.length * 2;
			if (newLength > marklimit) {
				newLength = marklimit;
			}
			byte[] newbuf = new byte[newLength];
			System.arraycopy(localBuf, 0, newbuf, 0, localBuf.length);
			// Reassign buf, which will invalidate any local references
			// FIXME: what if buf was null?
			localBuf = buf = newbuf;
		} else if (markpos > 0) {
			System.arraycopy(localBuf, markpos, localBuf, 0, localBuf.length
				- markpos);
		}
        /* Set the new position and mark position */
		pos -= markpos;
		count = markpos = 0;
		int bytesread = localIn.read(localBuf, pos, localBuf.length - pos);
		count = bytesread <= 0 ? pos : pos + bytesread;
		return bytesread;
	}

	/**
	 * Sets a mark position in this stream. The parameter {@code readlimit}
	 * indicates how many bytes can be read before a mark is invalidated.
	 * Calling {@code reset()} will reposition the stream back to the marked
	 * position if {@code readlimit} has not been surpassed. The underlying
	 * buffer may be increased in size to allow {@code readlimit} number of
	 * bytes to be supported.
	 *
	 * @param readlimit
	 *            the number of bytes that can be read before the mark is
	 *            invalidated.
	 * @see #reset()
	 */
	@Override
	public synchronized void mark(int readlimit) {
		marklimit = readlimit;
		markpos = pos;
	}

	/**
	 * Indicates whether {@code BufferedInputStream} supports the {@code mark()}
	 * and {@code reset()} methods.
	 *
	 * @return {@code true} for BufferedInputStreams.
	 * @see #mark(int)
	 * @see #reset()
	 */
	@Override
	public boolean markSupported() {
		return true;
	}

	/**
	 * Reads a single byte from this stream and returns it as an integer in the
	 * range from 0 to 255. Returns -1 if the end of the source string has been
	 * reached. If the internal buffer does not contain any available bytes then
	 * it is filled from the source stream and the first byte is returned.
	 *
	 * @return the byte read or -1 if the end of the source stream has been
	 *         reached.
	 * @throws IOException
	 *             if this stream is closed or another IOException occurs.
	 */
	@Override
	public synchronized int read() throws IOException {
		// Use local refs since buf and in may be invalidated by an
		// unsynchronized close()
		byte[] localBuf = buf;
		InputStream localIn = in;
		if (localBuf == null || localIn == null) {
			throw streamClosed();
		}

        /* Are there buffered bytes available? */
		if (pos >= count && fillbuf(localIn, localBuf) == -1) {
			return -1; /* no, fill buffer */
		}
		// localBuf may have been invalidated by fillbuf
		if (localBuf != buf) {
			localBuf = buf;
			if (localBuf == null) {
				throw streamClosed();
			}
		}

        /* Did filling the buffer fail with -1 (EOF)? */
		if (count - pos > 0) {
			return localBuf[pos++] & 0xFF;
		}
		return -1;
	}

	@Override public synchronized int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
		// Use local ref since buf may be invalidated by an unsynchronized
		// close()
		byte[] localBuf = buf;
		if (localBuf == null) {
			throw streamClosed();
		}
		if (byteCount == 0) {
			return 0;
		}
		InputStream localIn = in;
		if (localIn == null) {
			throw streamClosed();
		}

		int required;
		if (pos < count) {
            /* There are bytes available in the buffer. */
			int copylength = count - pos >= byteCount ? byteCount : count - pos;
			System.arraycopy(localBuf, pos, buffer, byteOffset, copylength);
			pos += copylength;
			if (copylength == byteCount || localIn.available() == 0) {
				return copylength;
			}
			byteOffset += copylength;
			required = byteCount - copylength;
		} else {
			required = byteCount;
		}

		while (true) {
			int read;
            /*
             * If we're not marked and the required size is greater than the
             * buffer, simply read the bytes directly bypassing the buffer.
             */
			if (markpos == -1 && required >= localBuf.length) {
				read = localIn.read(buffer, byteOffset, required);
				if (read == -1) {
					return required == byteCount ? -1 : byteCount - required;
				}
			} else {
				if (fillbuf(localIn, localBuf) == -1) {
					return required == byteCount ? -1 : byteCount - required;
				}
				// localBuf may have been invalidated by fillbuf
				if (localBuf != buf) {
					localBuf = buf;
					if (localBuf == null) {
						throw streamClosed();
					}
				}

				read = count - pos >= required ? required : count - pos;
				System.arraycopy(localBuf, pos, buffer, byteOffset, read);
				pos += read;
			}
			required -= read;
			if (required == 0) {
				return byteCount;
			}
			if (localIn.available() == 0) {
				return byteCount - required;
			}
			byteOffset += read;
		}
	}

	/**
	 * Resets this stream to the last marked location.
	 *
	 * @throws IOException
	 *             if this stream is closed, no mark has been set or the mark is
	 *             no longer valid because more than {@code readlimit} bytes
	 *             have been read since setting the mark.
	 * @see #mark(int)
	 */
	@Override
	public synchronized void reset() throws IOException {
		if (buf == null) {
			throw new IOException("Stream is closed");
		}
		if (-1 == markpos) {
			throw new IOException("Mark has been invalidated.");
		}
		pos = markpos;
	}

	/**
	 * Skips {@code byteCount} bytes in this stream. Subsequent calls to
	 * {@code read} will not return these bytes unless {@code reset} is
	 * used.
	 *
	 * @param byteCount
	 *            the number of bytes to skip. {@code skip} does nothing and
	 *            returns 0 if {@code byteCount} is less than zero.
	 * @return the number of bytes actually skipped.
	 * @throws IOException
	 *             if this stream is closed or another IOException occurs.
	 */
	@Override
	public synchronized long skip(long byteCount) throws IOException {
		// Use local refs since buf and in may be invalidated by an
		// unsynchronized close()
		byte[] localBuf = buf;
		InputStream localIn = in;
		if (localBuf == null) {
			throw streamClosed();
		}
		if (byteCount < 1) {
			return 0;
		}
		if (localIn == null) {
			throw streamClosed();
		}

		if (count - pos >= byteCount) {
			pos += byteCount;
			return byteCount;
		}
		long read = count - pos;
		pos = count;

		if (markpos != -1) {
			if (byteCount <= marklimit) {
				if (fillbuf(localIn, localBuf) == -1) {
					return read;
				}
				if (count - pos >= byteCount - read) {
					pos += byteCount - read;
					return byteCount;
				}
				// Couldn't get all the bytes, skip what we read
				read += (count - pos);
				pos = count;
				return read;
			}
		}
		return read + localIn.skip(byteCount - read);
	}
}
